﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Drawing;
using Processing;
using RayTracer.Kernels;
using RayTracer.Environment;
using Processing.Structures;
using General;
using System.Runtime.InteropServices;

namespace RayTracer
{
    /// <summary>
    /// Die Hauptstruktur des Raytracers
    /// </summary>
    public class RayTracer : IDisposable
    {
        #region Typen
        /// <summary>
        /// Ein Delegate für einfache Operationen auf Basis eines Raytracers.
        /// </summary>
        /// <param name="sender">Der Raytracer.</param>
        public delegate void SimpleEvent(RayTracer sender);

        /// <summary>
        /// Repräsentiert einen Status des Raytracers.
        /// </summary>
        public enum RayTracerState
        {
            /// <summary>
            /// Gibt an dass der Raytracer im Ruhezustand ist.
            /// </summary>
            Idle,

            /// <summary>
            /// Gibt an dass der Raytracer im gestoppten Zustand ist.
            /// </summary>
            Stopped,

            /// <summary>
            /// Gibt an dass der Raytracer im laufenden Zustand ist.
            /// </summary>
            Running
        }
        #endregion

        #region Attribute
        private Processor processor;
        private Size resolution;
        private Thread thread;
        private AutoResetEvent waitHandle;
        private Bitmap currentFrame;
        private object currentFrameSync = new object();
        private Scene scene;
        private bool sceneUpdateEnqueued = true;
        private object sceneSync = new object();
        private int maxThreadMemory = 256 * 1024 * 1024;
        private int maxDepth;
        private AutoResetEvent waitPictureHandle;
        private object waitPictureHandleSync = new object();
        private RayTracerState state;

        #region Serialized SceneData for Performanceenhancement
        // Hier wird die Komplette Szene zwischengelagert um nicht mehrfach die selben Berechnungen durchzuführen.
        #region Lichter
        #region SimpleLight
        private Buffer<Float4> sceneLightSimpleLightsPositions;
        private Buffer<Float4> sceneLightSimpleLightsColors;
        #endregion SimpleLight
        #endregion Lichter
        #region Objekte
        #region Kugeln
        private Buffer<Float4> sceneObjectSpheresPositions;
        private Buffer<Float4> sceneObjectSpheresRotations;
        private Buffer<float> sceneObjectSpheresRadiusses;
        private Buffer<Float16> sceneObjectSpheresMaterials;
        #endregion Kugeln
        #endregion Objekte
        #endregion Serialized SceneData for Performanceenhancement
        #endregion Attribute

        #region Eigenschaften
        /// <summary>
        /// Ereignis das Eintritt wenn eine Frame fertiggestellt wurde.
        /// </summary>
        public event SimpleEvent FrameDone;

        /// <summary>
        /// Ereignis das Eintritt wenn der Raytracer gestartet wird.
        /// </summary>
        public event SimpleEvent Started;

        /// <summary>
        /// Ereignis das Eintritt wen nder Raytracer gestoppt wird.
        /// </summary>
        public event SimpleEvent Stopped;

        /// <summary>
        /// Gibt den verwendeten Prozessor an.
        /// </summary>
        public Processor Processor { get { return processor; } }

        /// <summary>
        /// Gibt und setzt die Auflösung der Frame.
        /// </summary>
        public Size Resolution
        {
            get { return resolution; }
            set
            {
                if (value.Height == 0 || value.Width == 0 || value.IsEmpty) throw new ArgumentException("Die Auflösung ist ungültig.");
                resolution = value;
            }
        }

        /// <summary>
        /// Gibt und setzt die maximale Rekursionstiefe.
        /// </summary>
        public int MaxDepth
        {
            get { return maxDepth; }
            set
            {
                if (value <= 0 || value > 128) throw new ArgumentException("Die maximale Rekursionstiefe sollte zwischen 1 und 128 liegen.");
                maxDepth = value;
            }
        }

        /// <summary>
        /// Gibt und setzt den maximal verwendbaren Threadspeicher in Bytes.
        /// </summary>
        public int MaxThreadMemory
        {
            get { return maxThreadMemory; }
            set
            {
                if (value < 1024) throw new ArgumentException("Es wird zumindest 1 Kb zum halten von Daten benötigt.");
                maxThreadMemory = value;
            }
        }

        /// <summary>
        /// Gibt die aktuelle Frame an.
        /// </summary>
        public Bitmap CurrentFrame { get { lock (currentFrameSync) { return currentFrame; } } }

        /// <summary>
        /// Gibt und Setzt die Szene.
        /// </summary>
        public Scene Scene
        {
            get { lock (sceneSync) { return scene; } }
            set
            {
                if (value == null) throw new ArgumentException("Der Wert darf nicht null sein.", "value");
                lock (sceneSync) { scene = value; }
            }
        }

        /// <summary>
        /// Gibt den Status des Raytracers an.
        /// </summary>
        public RayTracerState State
        {
            get { return state; }
        }
        #endregion Eigenschaften

        #region Internals
        #region Serialized SceneData for Performanceenhancement
        // Hier wird die Komplette Szene zwischengelagert um nicht mehrfach die selben Berechnungen durchzuführen.
        #region Lichter
        #region SimpleLight
        /// <summary>
        /// Gibt den Array aller Positionen aller einfachen Lichter der Szene zurück.
        /// </summary>
        internal Buffer<Float4> SceneLightSimpleLightsPositions { get { return sceneLightSimpleLightsPositions; } }

        /// <summary>
        /// Gibt den Array aller Farben aller einfachen Lichter der Szene zurück.
        /// </summary>
        internal Buffer<Float4> SceneLightSimpleLightsColors { get { return sceneLightSimpleLightsColors; } }
        #endregion SimpleLight
        #endregion Lichter
        #region Objekte
        #region Kugeln
        /// <summary>
        /// Gibt den Array aller Positionen aller Kugeln der Szene zurück.
        /// </summary>
        internal Buffer<Float4> SceneObjectSpheresPositions { get { return sceneObjectSpheresPositions; } }

        /// <summary>
        /// Gibt den Array aller Rotationen aller Kugeln der Szene zurück.
        /// </summary>
        internal Buffer<Float4> SceneObjectSpheresRotations { get { return sceneObjectSpheresRotations; } }

        /// <summary>
        /// Gibt den Array aller Radien aller Kugeln der Szene zurück.
        /// </summary>
        internal Buffer<float> SceneObjectSpheresRadiusses { get { return sceneObjectSpheresRadiusses; } }

        /// <summary>
        /// Gibt den Array aller Materialen aller Kugeln der Szene zurück.
        /// </summary>
        internal Buffer<Float16> SceneObjectSpheresMaterials { get { return sceneObjectSpheresMaterials; } }
        #endregion Kugeln
        #endregion Objekte
        #endregion Serialized SceneData for Performanceenhancement
        #endregion Internals

        #region Methoden
        /// <summary>
        /// Instanziiert und initialisiert die Klasse.
        /// </summary>
        /// <param name="processor">Die Recheneinheit auf der der Raytracer rechnen soll.</param>
        /// <param name="resolution">Die Auflösung des Bildes.</param>
        /// <param name="maxDepth">Gibt die maximale Rekursionstiefe an.</param>
        public RayTracer(Processor processor, Size resolution, int maxDepth)
        {
            if (processor == null) throw new ArgumentNullException("processor");

            Console.WriteLine("Initialisiere Raytracer ...");
            TimeClock.Start("RayTracerInit");
            MaxDepth = maxDepth;
            Resolution = resolution;
            this.processor = processor;
            this.thread = new Thread(runtime, MaxThreadMemory);
            this.thread.Name = "Raytracer";
            this.waitHandle = new AutoResetEvent(false);
            waitHandle.Reset();
            this.waitPictureHandle = new AutoResetEvent(false);
            waitPictureHandle.Reset();

            this.currentFrame = new Bitmap(resolution.Width, resolution.Height);
            this.Scene = new Scene();

            #region Kernelkompilierung
            new KernelPrimitiveSphere(processor);
            new KernelSimpleLightPrimitiveSphere(processor);
            new KernelInitCamera(processor);
            new KernelRaySplit(processor);
            new KernelSumColor(processor);
            #endregion Kernelkompilierung
            TimeClock.Stop("RayTracerInit", "Die Initialisierung hat", "Sekunden benötigt.");
        }

        /// <summary>
        /// Führt die Runtime aus.
        /// </summary>
        public void Start()
        {
            if (state == RayTracerState.Idle)
            {
                Console.WriteLine("Raytracer gestartet ..");
                sceneUpdateEnqueued = true;
                state = RayTracerState.Running;
                thread.Start();
                if (Started != null) Started(this);
            }
            else throw new RayTracerException("Der Raytracer läuft bereits.");
        }

        /// <summary>
        /// Stoppt die Runtime.
        /// </summary>
        public void Stop()
        {
            if (state == RayTracerState.Running)
            {
                state = RayTracerState.Stopped;
                waitHandle.WaitOne();
                if (Stopped != null) Stopped(this);
                state = RayTracerState.Idle;
            }
            else throw new RayTracerException("Der Raytracer muss erst laufen.");
        }

        /// <summary>
        /// Die Laufzeit des Raytracers.
        /// </summary>
        private void runtime()
        {
            while (state == RayTracerState.Running)
            {
                Size resolution = this.resolution;  // Damit für das aktuelle Bild immer gleich
                updateScalarSceneData();
                try
                {
                    TimeClock.Start("RayTracerRender");
                    Bitmap currentFrame = new Bitmap(resolution.Width, resolution.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);

                    // Wenn keine Objekte da sind muss nichts gerendert werden.
                    // Dies erspärt zudem weiterere Prüfungen während der Laufzeit
                    if (Scene.Objects.Length > 0 && Scene.Lights.Length > 0)
                    {
                        // Sehstrahlen und initiale Distanz festlegen.
                        KernelInitCamera kic = new KernelInitCamera(processor);
                        kic.ScreenResolution = resolution.Serialize();
                        kic.CameraPosition = scene.Camera.Position.Serialize();
                        kic.CameraFocus = scene.Camera.Metrics.Focus;
                        kic.FovXAxis = scene.Camera.Metrics.XAxis.Serialize();
                        kic.FovYAxis = scene.Camera.Metrics.YAxis.Serialize();
                        kic.FovZAxis = scene.Camera.Metrics.ZAxis.Serialize();
                        kic.Run();

                        // Erste Strahlen hier rein.
                        RayTracerRecursion rtr = new RayTracerRecursion(this, maxDepth, new RayTracerRecursion.Input(kic.RayPositions, kic.RayDicrections));
                        RayTracerRecursion.Output result = rtr.Finish();
                        kic.RayPositions.Dispose();
                        kic.RayDicrections.Dispose();
                        kic.Dispose();
                        rtr.Dispose();

                        if (result.Colors == null || result.Colors.Data == null) throw new RayTracerException("Rendern des Bildes fehlgeschlagen.");

                        // Farben umwandeln in Byte.
                        KernelConvertColor kcc = new KernelConvertColor(processor);
                        kcc.FloatingColors = result.Colors;
                        kcc.Run();
                        result.Colors.Dispose();
                        result.Distances.Dispose();

                        // Farbzuweisung
                        GCHandle hData = GCHandle.Alloc(kcc.ResultColors.Data, GCHandleType.Pinned);
                        byte[] data = new byte[kcc.ResultColors.Data.Length * 4];
                        Marshal.Copy(hData.AddrOfPinnedObject(), data, 0, kcc.ResultColors.Data.Length * 4);
                        hData.Free();
                        System.Drawing.Imaging.BitmapData bdata = currentFrame.LockBits(
                            new Rectangle(Point.Empty, currentFrame.Size),
                            System.Drawing.Imaging.ImageLockMode.WriteOnly,
                            System.Drawing.Imaging.PixelFormat.Format32bppArgb);
                        Marshal.Copy(data, 0, bdata.Scan0, kcc.ResultColors.Data.Length * 4);
                        data = null;
                        currentFrame.UnlockBits(bdata);
                        kcc.ResultColors.Dispose();
                    }
                    lock (currentFrameSync) { this.currentFrame = currentFrame; }

                    lock (waitPictureHandleSync) waitPictureHandle.Set();
                    if (state == RayTracerState.Running)
                    {
                        TimeClock.Stop("RayTracerRender", "\nFrame gerendert in", "Sekunden.");
                        if (FrameDone != null) FrameDone(this);
                    }
                    GC.Collect();   // Muss explizit stattfinden, da langsamere Maschinen bzw. die kein echtes Multithreading unterstützen sonst der Speicher voll läuft.
                }
                catch (Exception e)
                {
                    if (state == RayTracerState.Stopped) state = RayTracerState.Idle;
                    else General.ConsolePrinter.PrintExceptionStack(e);
                }
            }
            if (state == RayTracerState.Stopped) state = RayTracerState.Idle;
            waitHandle.Set();
        }

        #region SceneBuffers
        /// <summary>
        /// Stößt eine Szenenaktualisierung für das nächste gerenderte Bild an.
        /// </summary>
        public void UpdateScene()
        {
            sceneUpdateEnqueued = true;
        }

        /// <summary>
        /// Aktualisert die skalaren Szenendaten.
        /// </summary>
        private void updateScalarSceneData()
        {
            if (sceneUpdateEnqueued)
            {
                allocateSceneBuffers();
                sceneUpdateEnqueued = false;
            }
        }

        /// <summary>
        /// Allokiert die Szenenbuffer und wandelt die Szenendaten in skalare Daten um.
        /// </summary>
        private void allocateSceneBuffers()
        {
            lock (sceneSync)
            {
                releaseSceneBuffers();
                #region Lichter
                #region SimpleLight
                try
                {
                    sceneLightSimpleLightsPositions = processor.AllocateBuffer(scene.SimpleLights.GetSerializedPositions(), true);
                    sceneLightSimpleLightsColors = processor.AllocateBuffer(scene.SimpleLights.GetSerializedColors(), true);
                }
                catch { }
                #endregion SimpleLight
                #endregion Lichter
                #region Objekte
                #region Kugeln
                try
                {
                    sceneObjectSpheresPositions = processor.AllocateBuffer(scene.Spheres.GetSerializedPositions(), true);
                    sceneObjectSpheresRotations = processor.AllocateBuffer(scene.Spheres.GetSerializedRotations(), true);
                    sceneObjectSpheresRadiusses = processor.AllocateBuffer(scene.Spheres.GetSerializedRadiuses(), true);
                    sceneObjectSpheresMaterials = processor.AllocateBuffer(scene.Spheres.GetSerializedMaterials(), true);
                }
                catch { }
                #endregion Kugeln
                #endregion Objekte
            }
        }

        /// <summary>
        /// Lässt die Szenenbuffer frei.
        /// </summary>
        private void releaseSceneBuffers()
        {
            lock (sceneSync)
            {
                #region Lichter
                #region SimpleLight
                if (sceneObjectSpheresPositions != null)
                {
                    sceneObjectSpheresPositions.Dispose();
                    sceneObjectSpheresPositions = null;
                }
                if (sceneObjectSpheresRotations != null)
                {
                    sceneObjectSpheresRotations.Dispose();
                    sceneObjectSpheresRotations = null;
                }
                #endregion SimpleLight
                #endregion Lichter
                #region Objekte
                #region Kugeln
                if (sceneObjectSpheresRadiusses != null)
                {
                    sceneObjectSpheresRadiusses.Dispose();
                    sceneObjectSpheresRadiusses = null;
                }
                if (sceneObjectSpheresMaterials != null)
                {
                    sceneObjectSpheresMaterials.Dispose();
                    sceneObjectSpheresMaterials = null;
                }
                if (sceneLightSimpleLightsPositions != null)
                {
                    sceneLightSimpleLightsPositions.Dispose();
                    sceneLightSimpleLightsPositions = null;
                }
                if (sceneLightSimpleLightsColors != null)
                {
                    sceneLightSimpleLightsColors.Dispose();
                    sceneLightSimpleLightsColors = null;
                }
                #endregion Kugeln
                #endregion Objekte
            }
        }
        #endregion SceneBuffers

        /// <summary>
        /// Gibt das nächste gerenderte Bild zurück und blockt bis dieses gerendert wurde.
        /// </summary>
        /// <returns>Das nächste Bild.</returns>
        public Bitmap GetNextImage()
        {
            waitPictureHandle.WaitOne();
            lock (waitPictureHandleSync)
            {
                waitPictureHandle = new AutoResetEvent(false);  // Muss hier neu erzeugt werden, da ein Reset ggf. andere Listener ebenfalls warten die sonst ggf. stehen bleiben.
                waitPictureHandle.Reset();
            }
            return currentFrame;
        }

        /// <summary>
        /// Gibt die minimale Menge an benötigtem Speicher für eine spezifische Auflösung bei einer spezifischen maximalen Rekursionstiefe an.
        /// </summary>
        /// <param name="maxDepth">Die maximale Rekursionstiefe.</param>
        /// <param name="resolution">Die Auflösung.</param>
        /// <returns>Der minimal benötigte Speicher in Bytes.</returns>
        public static long GetMinRequiredMemory(int maxDepth, Size resolution)
        {
            return (long)resolution.Width * (long)resolution.Height *
                    ((long)Kernels.KernelInitCamera.BytesPerPixel +
                        ((long)maxDepth * ((long)Kernels.KernelPrimitiveSphere.BytesPerPixel +
                                     (long)Kernels.KernelRaySplit.BytesPerPixel +
                                     (long)Kernels.KernelSimpleLightPrimitiveSphere.BytesPerPixel +
                                     (long)Kernels.KernelSumColor.BytesPerPixel))
                    );
        }

        /// <summary>
        /// Überlässt das Objekt dem Garbagecollector.
        /// </summary>
        public void Dispose()
        {
            processor.Dispose();
            releaseSceneBuffers();

            waitHandle = null;
            waitPictureHandle = null;
            waitPictureHandleSync = null;
        }
        #endregion Methoden
    }
}
